Fixed leakage of all parts of the chat heirarchy including the webview when closing...
[adiumx.git] / Plugins / Dual Window Interface / AIMessageViewController.m
blob35aeffa7f087916975a38151c780113acba44579
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "AIMessageViewController.h"
18 #import "AIAccountSelectionView.h"
19 #import "AIMessageWindowController.h"
20 #import "ESGeneralPreferencesPlugin.h"
21 #import "AIDualWindowInterfacePlugin.h"
22 #import "AIContactInfoWindowController.h"
23 #import "AIMessageTabSplitView.h"
25 #import <Adium/AIChatControllerProtocol.h>
26 #import <Adium/AIContactAlertsControllerProtocol.h>
27 #import <Adium/AIContactControllerProtocol.h>
28 #import <Adium/AIContentControllerProtocol.h>
29 #import <Adium/AIContentControllerProtocol.h>
30 #import <Adium/AIInterfaceControllerProtocol.h>
31 #import <Adium/AIMenuControllerProtocol.h>
32 #import <Adium/AIPreferenceControllerProtocol.h>
33 #import <Adium/AIToolbarControllerProtocol.h>
34 #import <Adium/AIAccount.h>
35 #import <Adium/AIChat.h>
36 #import <Adium/AIContentMessage.h>
37 #import <Adium/AIListContact.h>
38 #import <Adium/AIListObject.h>
39 #import <Adium/AIListOutlineView.h>
40 #import <Adium/AIMessageEntryTextView.h>
41 #import <Adium/ESTextAndButtonsWindowController.h>
43 #import <AIUtilities/AIApplicationAdditions.h>
44 #import <AIUtilities/AIAttributedStringAdditions.h>
45 #import <AIUtilities/AIAutoScrollView.h>
46 #import <AIUtilities/AIDictionaryAdditions.h>
47 #import <AIUtilities/AISplitView.h>
49 #import <AIUtilities/AITigerCompatibility.h>
51 #import <PSMTabBarControl/NSBezierPath_AMShading.h>
52 #import "KNShelfSplitView.h"
53 #import "ESChatUserListController.h"
55 //Heights and Widths
56 #define MESSAGE_VIEW_MIN_HEIGHT_RATIO           .50                                             //Mininum height ratio of the message view
57 #define MESSAGE_VIEW_MIN_WIDTH_RATIO            .50                                             //Mininum width ratio of the message view
58 #define ENTRY_TEXTVIEW_MIN_HEIGHT                       20                                              //Mininum height of the text entry view
59 #define USER_LIST_MIN_WIDTH                                     24                                              //Mininum width of the user list
60 #define USER_LIST_DEFAULT_WIDTH                         120                                             //Default width of the user list
62 //Preferences and files
63 #define MESSAGE_VIEW_NIB                                        @"MessageView"                  //Filename of the message view nib
64 #define USERLIST_THEME                                          @"UserList Theme"               //File name of the user list theme
65 #define USERLIST_LAYOUT                                         @"UserList Layout"              //File name of the user list layout
66 #define KEY_ENTRY_TEXTVIEW_MIN_HEIGHT           @"Minimum Text Height"  //Preference key for text entry height
67 #define KEY_ENTRY_USER_LIST_MIN_WIDTH           @"UserList Width"               //Preference key for user list width
70 @interface AIMessageViewController (PRIVATE)
71 - (id)initForChat:(AIChat *)inChat;
72 - (void)chatStatusChanged:(NSNotification *)notification;
73 - (void)chatParticipatingListObjectsChanged:(NSNotification *)notification;
74 - (void)_configureMessageDisplay;
75 - (void)_createAccountSelectionView;
76 - (void)_destroyAccountSelectionView;
77 - (void)_configureTextEntryView;
78 - (void)_updateTextEntryViewHeight;
79 - (int)_textEntryViewProperHeightIgnoringUserMininum:(BOOL)ignoreUserMininum;
80 - (void)_showUserListView;
81 - (void)_hideUserListView;
82 - (void)_configureUserList;
83 - (void)_updateUserListViewWidth;
84 - (int)_userListViewProperWidthIgnoringUserMininum:(BOOL)ignoreUserMininum;
85 - (void)updateFramesForAccountSelectionView;
86 - (void)saveUserListMinimumSize;
87 @end
89 @implementation AIMessageViewController
91 /*!
92  * @brief Create a new message view controller
93  */
94 + (AIMessageViewController *)messageDisplayControllerForChat:(AIChat *)inChat
96     return [[[self alloc] initForChat:inChat] autorelease];
101  * @brief Initialize
102  */
103 - (id)initForChat:(AIChat *)inChat
105     if ((self = [super init])) {
106                 AIListContact   *contact;
107                 //Init
108                 chat = [inChat retain];
109                 contact = [chat listObject];
110                 view_accountSelection = nil;
111                 userListController = nil;
112                 suppressSendLaterPrompt = NO;
113                 retainingScrollViewUserList = NO;
114                 
115                 //Load the view containing our controls
116                 [NSBundle loadNibNamed:MESSAGE_VIEW_NIB owner:self];
117                 
118                 //Register for the various notification we need
119                 [[adium notificationCenter] addObserver:self
120                                                                            selector:@selector(sendMessage:) 
121                                                                                    name:Interface_SendEnteredMessage
122                                                                                  object:chat];
123                 [[adium notificationCenter] addObserver:self
124                                                                            selector:@selector(didSendMessage:)
125                                                                                    name:Interface_DidSendEnteredMessage 
126                                                                                  object:chat];
127                 [[adium notificationCenter] addObserver:self
128                                                                            selector:@selector(chatStatusChanged:) 
129                                                                                    name:Chat_StatusChanged
130                                                                                  object:chat];
131                 [[adium notificationCenter] addObserver:self 
132                                                                            selector:@selector(chatParticipatingListObjectsChanged:)
133                                                                                    name:Chat_ParticipatingListObjectsChanged
134                                                                                  object:chat];
135                 [[adium notificationCenter] addObserver:self
136                                                                            selector:@selector(redisplaySourceAndDestinationSelector:) 
137                                                                                    name:Chat_SourceChanged
138                                                                                  object:chat];
139                 [[adium notificationCenter] addObserver:self
140                                                                            selector:@selector(redisplaySourceAndDestinationSelector:) 
141                                                                                    name:Chat_DestinationChanged
142                                                                                  object:chat];
143                 [[adium notificationCenter] addObserver:self
144                                                                            selector:@selector(toggleUserlist:)
145                                                                                    name:@"toggleUserlist"
146                                                                                  object:nil];
147                 
148                 [splitView_textEntryHorizontal setDividerThickness:3]; //Default is 9
149                 [splitView_textEntryHorizontal setDrawsDivider:NO];
150                 
151                 //Observe general preferences for sending keys
152                 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_GENERAL];
154                 /* Update chat status and participating list objects to configure the user list if necessary
155                  * Call chatParticipatingListObjectsChanged first, which will set up the user list. This allows other sizing to match.
156                  */
157                 [self setUserListVisible:[chat isGroupChat]];
158                 
159                 [self chatParticipatingListObjectsChanged:nil];
160                 [self chatStatusChanged:nil];
161                 
162                 //Configure our views
163                 [self _configureMessageDisplay];
164                 [self _configureTextEntryView];
166                 //Set our base writing direction
167                 if (contact) {
168                         [textView_outgoing setBaseWritingDirection:[contact baseWritingDirection]];
169                 }
170         }
172         return self;
176  * @brief Deallocate
177  */
178 - (void)dealloc
179 {   
180         AIListContact   *contact = [chat listObject];
181         
182         [[adium preferenceController] unregisterPreferenceObserver:self];
184         //Store our minimum height for the text entry area, and minimim width for the user list
185         [[adium preferenceController] setPreference:[NSNumber numberWithInt:entryMinHeight]
186                                                                                  forKey:KEY_ENTRY_TEXTVIEW_MIN_HEIGHT
187                                                                                   group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
189         if (userListController) {
190                 [self saveUserListMinimumSize];
191         }
192         
193         //Save the base writing direction
194         if (contact)
195                 [contact setBaseWritingDirection:[textView_outgoing baseWritingDirection]];
197         [chat release]; chat = nil;
199     //remove observers
200     [[adium notificationCenter] removeObserver:self];
201     [[NSNotificationCenter defaultCenter] removeObserver:self];
202         
203     //Account selection view
204         [self _destroyAccountSelectionView];
205         
206         [messageDisplayController messageViewIsClosing];
207     [messageDisplayController release];
208         [userListController release];
210         [controllerView_messages release];
211         
212         //Release the views for which we are responsible (because we loaded them via -[NSBundle loadNibNamed:owner])
213         [nibrootView_messageView release];
214         [nibrootView_shelfVew release];
215         [nibrootView_userList release];
217         //Release the hidden user list view
218         if (retainingScrollViewUserList) {
219                 [scrollView_userList release];
220         }
221         //release menuItem
222         [showHide release];
223     [super dealloc];
226 - (void)saveUserListMinimumSize
228         [[adium preferenceController] setPreference:[NSNumber numberWithInt:userListMinWidth]
229                                                                                  forKey:KEY_ENTRY_USER_LIST_MIN_WIDTH
230                                                                                   group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
233 - (void)updateGradientColors
235         NSColor *darkerColor = [NSColor colorWithCalibratedWhite:0.90 alpha:1.0];
236         NSColor *lighterColor = [NSColor colorWithCalibratedWhite:0.92 alpha:1.0];
237         NSColor *leftColor = nil, *rightColor = nil;
239         switch ([messageWindowController tabPosition]) {
240                 case AdiumTabPositionBottom:
241                 case AdiumTabPositionTop:
242                 case AdiumTabPositionLeft:
243                         leftColor = lighterColor;
244                         rightColor = darkerColor;
245                         break;
246                 case AdiumTabPositionRight:
247                         leftColor = darkerColor;
248                         rightColor = lighterColor;
249                         break;
250         }
252         [view_accountSelection setLeftColor:leftColor rightColor:rightColor];
253         [splitView_textEntryHorizontal setLeftColor:leftColor rightColor:rightColor];
257  * @brief Invoked before the message view closes
259  * This method is invoked before our message view controller's message view leaves a window.
260  * We need to clean up our user list to invalidate cursor tracking before the view closes.
261  */
262 - (void)messageViewWillLeaveWindowController:(AIMessageWindowController *)inWindowController
264         if (inWindowController) {
265                 [userListController contactListWillBeRemovedFromWindow];
266         }
267         
268         [messageWindowController release]; messageWindowController = nil;
271 - (void)messageViewAddedToWindowController:(AIMessageWindowController *)inWindowController
273         if (inWindowController) {
274                 [userListController contactListWasAddedBackToWindow];
275         }
276         
277         if (inWindowController != messageWindowController) {
278                 [messageWindowController release];
279                 messageWindowController = [inWindowController retain];
280                 
281                 [self updateGradientColors];
282         }
286  * @brief Retrieve the chat represented by this message view
287  */
288 - (AIChat *)chat
290     return chat;
294  * @brief Retrieve the source account associated with this chat
295  */
296 - (AIAccount *)account
298     return [chat account];
302  * @brief Retrieve the destination list object associated with this chat
303  */
304 - (AIListContact *)listObject
306     return [chat listObject];
310  * @brief Returns the selected list object in our participants list
311  */
312 - (AIListObject *)preferredListObject
314         if (userListView) { //[[shelfView subviews] containsObject:scrollView_userList] && ([userListView selectedRow] != -1)
315                 return [userListView itemAtRow:[userListView selectedRow]];
316         }
317         
318         return nil;
322  * @brief Invoked when the status of our chat changes
324  * The only chat status change we're interested in is one to the disallow account switching flag.  When this flag 
325  * changes we update the visibility of our account status menus accordingly.
326  */
327 - (void)chatStatusChanged:(NSNotification *)notification
329     NSArray     *modifiedKeys = [[notification userInfo] objectForKey:@"Keys"];
330         
331     if (notification == nil || [modifiedKeys containsObject:@"DisallowAccountSwitching"]) {
332                 [self setAccountSelectionMenuVisibleIfNeeded:YES];
333     }
337 //Message Display ------------------------------------------------------------------------------------------------------
338 #pragma mark Message Display
340  * @brief Configure the message display view
341  */
342 - (void)_configureMessageDisplay
344         //Create the message view
345         messageDisplayController = [[[adium interfaceController] messageDisplayControllerForChat:chat] retain];
346         //Get the messageView from the controller
347         controllerView_messages = [[messageDisplayController messageView] retain];
348         //scrollView_messages is originally a placeholder; replace it with controllerView_messages
349         [controllerView_messages setFrame:[scrollView_messages documentVisibleRect]];
350         [[customView_messages superview] replaceSubview:customView_messages with:controllerView_messages];
352         //This is what draws our transparent background
353         //Technically, it could be set in MessageView.nib, too
354         [scrollView_messages setBackgroundColor:[NSColor clearColor]];
356         [controllerView_messages setNextResponder:textView_outgoing];
360  * @brief Access to our view
361  */
362 - (NSView *)view
364     return view_contents;
368  * @brief Support for printing.  Forward the print command to our message display view
369  */
370 - (void)adiumPrint:(id)sender
372         if ([messageDisplayController respondsToSelector:@selector(adiumPrint:)]) {
373                 [messageDisplayController adiumPrint:sender];
374         }
378 //Messaging ------------------------------------------------------------------------------------------------------------
379 #pragma mark Messaging
381  * @brief Send the entered message
382  */
383 - (IBAction)sendMessage:(id)sender
385         NSAttributedString      *attributedString = [textView_outgoing textStorage];
386         
387         //Only send if we have a non-zero-length string
388     if ([attributedString length] != 0) { 
389                 AIListObject                            *listObject = [chat listObject];
390                 
391                 if ([chat isGroupChat] && ![[chat account] online]) {
392                         //Refuse to do anything with a group chat for an offline account.
393                         NSBeep();
394                         return;
395                 }
396                 
397                 if (!suppressSendLaterPrompt &&
398                         ![chat canSendMessages]) {
399                         
400                         NSString                                                        *formattedUID = [listObject formattedUID];
402                         NSAlert *alert = [[NSAlert alloc] init];
403                         [alert setMessageText:[NSString stringWithFormat:AILocalizedString(@"%@ appears to be offline. How do you want to send this message?", nil),
404                                                                    formattedUID]];
405                         [alert setInformativeText:[NSString stringWithFormat:
406                                                                            AILocalizedString(@"Send Later will send the message the next time both you and %@ are online. Send Now may work if %@ is invisible or is not on your contact list and so only appears to be offline.", "Send Later dialogue explanation text"),
407                                                                            formattedUID, formattedUID, formattedUID]];
408                         [alert addButtonWithTitle:AILocalizedString(@"Send Now", nil)];
410                         [alert addButtonWithTitle:AILocalizedString(@"Send Later", nil)];
411                         [[[alert buttons] objectAtIndex:1] setKeyEquivalent:@"l"];
412                         [[[alert buttons] objectAtIndex:1] setKeyEquivalentModifierMask:0];
414                         [alert addButtonWithTitle:AILocalizedString(@"Don't Send", nil)];                        
415                         [[[alert buttons] objectAtIndex:2] setKeyEquivalent:@"\E"];
416                         [[[alert buttons] objectAtIndex:2] setKeyEquivalentModifierMask:0];
418                         NSImage *icon = ([listObject userIcon] ? [listObject userIcon] : [AIServiceIcons serviceIconForObject:listObject
419                                                                                                                                                                                                                          type:AIServiceIconLarge
420                                                                                                                                                                                                                 direction:AIIconNormal]);
421                         icon = [[icon copy] autorelease];
422                         [icon setScalesWhenResized:NO];
423                         [alert setIcon:icon];
424                         [alert setAlertStyle:NSInformationalAlertStyle];
426                         [alert beginSheetModalForWindow:[view_contents window]
427                                                           modalDelegate:self
428                                                          didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
429                                                                 contextInfo:NULL];
430                         [alert release];
433                 } else {
434                         AIContentMessage                *message;
435                         NSAttributedString              *outgoingAttributedString;
436                         AIAccount                               *account = [chat account];
437                         //Send the message
438                         [[adium notificationCenter] postNotificationName:Interface_WillSendEnteredMessage
439                                                                                                           object:chat
440                                                                                                         userInfo:nil];
442                         outgoingAttributedString = [attributedString copy];
443                         message = [AIContentMessage messageInChat:chat
444                                                                                    withSource:account
445                                                                                   destination:[chat listObject]
446                                                                                                  date:nil //created for us by AIContentMessage
447                                                                                           message:outgoingAttributedString
448                                                                                         autoreply:NO];
449                         [outgoingAttributedString release];
451                         if ([[adium contentController] sendContentObject:message]) {
452                                 [[adium notificationCenter] postNotificationName:Interface_DidSendEnteredMessage 
453                                                                                                                   object:chat
454                                                                                                                 userInfo:nil];
455                         }
456                 }
457     }
461  * @brief Send Later button was pressed
462  */ 
463 - (void)alertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
465         switch (returnCode) {
466                 case NSAlertFirstButtonReturn: /* Send Now */
467                         suppressSendLaterPrompt = YES;
468                         [self sendMessage:nil];
469                         break;
470                         
471                 case NSAlertSecondButtonReturn: /* Send Later */
472                         [self sendMessageLater:nil];
473                         break;
474                 case NSAlertThirdButtonReturn: /* Don't Send */
475                         break;          
476         }
480  * @brief Invoked after our entered message sends
482  * This method hides the account selection view and clears the entered message after our message sends
483  */
484 - (IBAction)didSendMessage:(id)sender
486     [self setAccountSelectionMenuVisibleIfNeeded:NO];
487     [self clearTextEntryView];
488         
489         //Redisplay the cursor
490         [NSCursor setHiddenUntilMouseMoves:NO];
494  * @brief Offline messaging
495  */
496 - (IBAction)sendMessageLater:(id)sender
498         AIListContact   *listContact;
500         //If the chat can _now_ send a message, send it immediately instead of waiting for "later".
501         if ([chat canSendMessages]) {
502                 [self sendMessage:sender];
503                 return;
504         }
506         //Put the alert on the metaContact containing this listContact if applicable
507         listContact = [[chat listObject] parentContact];
509         if (listContact) {
510                 NSMutableDictionary *detailsDict, *alertDict;
511                 
512                 detailsDict = [NSMutableDictionary dictionary];
513                 [detailsDict setObject:[[chat account] internalObjectID] forKey:@"Account ID"];
514                 [detailsDict setObject:[NSNumber numberWithBool:YES] forKey:@"Allow Other"];
515                 [detailsDict setObject:[listContact internalObjectID] forKey:@"Destination ID"];
517                 alertDict = [NSMutableDictionary dictionary];
518                 [alertDict setObject:detailsDict forKey:@"ActionDetails"];
519                 [alertDict setObject:CONTACT_SEEN_ONLINE_YES forKey:@"EventID"];
520                 [alertDict setObject:@"SendMessage" forKey:@"ActionID"];
521                 [alertDict setObject:[NSNumber numberWithBool:YES] forKey:@"OneTime"]; 
522                 
523                 [alertDict setObject:listContact forKey:@"TEMP-ListContact"];
524                 
525                 [[adium contentController] filterAttributedString:[[[textView_outgoing textStorage] copy] autorelease]
526                                                                                   usingFilterType:AIFilterContent
527                                                                                                 direction:AIFilterOutgoing
528                                                                                         filterContext:listContact
529                                                                                   notifyingTarget:self
530                                                                                                  selector:@selector(gotFilteredMessageToSendLater:receivingContext:)
531                                                                                                   context:alertDict];
533                 [self didSendMessage:nil];
534         }
538  * @brief Offline messaging
539  */
540 //XXX - Offline messaging code SHOULD NOT BE IN HERE! -ai
541 - (void)gotFilteredMessageToSendLater:(NSAttributedString *)filteredMessage receivingContext:(NSMutableDictionary *)alertDict
543         NSMutableDictionary     *detailsDict;
544         AIListContact           *listContact;
545         
546         detailsDict = [alertDict objectForKey:@"ActionDetails"];
547         [detailsDict setObject:[filteredMessage dataRepresentation] forKey:@"Message"];
549         listContact = [[alertDict objectForKey:@"TEMP-ListContact"] retain];
550         [alertDict removeObjectForKey:@"TEMP-ListContact"];
551         
552         [[adium contactAlertsController] addAlert:alertDict 
553                                                                  toListObject:listContact
554                                                          setAsNewDefaults:NO];
555         [listContact release];
558 //Account Selection ----------------------------------------------------------------------------------------------------
559 #pragma mark Account Selection
561  * @brief
562  */
563 - (void)accountSelectionViewFrameDidChange:(NSNotification *)notification
565         [self updateFramesForAccountSelectionView];
569  * @brief Redisplay the source/destination account selector
570  */
571 - (void)redisplaySourceAndDestinationSelector:(NSNotification *)notification
573         [self setAccountSelectionMenuVisibleIfNeeded:YES];
577  * @brief Toggle visibility of the account selection menus
579  * Invoking this method with NO will hide the account selection menus.  Invoking it with YES will show the account
580  * selection menus if they are needed.
581  */
582 - (void)setAccountSelectionMenuVisibleIfNeeded:(BOOL)makeVisible
584         //Hide or show the account selection view as requested
585         if (makeVisible) {
586                 [self _createAccountSelectionView];
587         } else {
588                 [self _destroyAccountSelectionView];
589         }
593  * @brief Show the account selection view
594  */
595 - (void)_createAccountSelectionView
597         if (!view_accountSelection) {
598                 NSRect  contentFrame = [splitView_textEntryHorizontal frame];
600                 //Create the account selection view and insert it into our window
601                 view_accountSelection = [[AIAccountSelectionView alloc] initWithFrame:contentFrame];
603                 [view_accountSelection setAutoresizingMask:(NSViewWidthSizable | NSViewMinYMargin)];
604                 
605                 [self updateGradientColors];
606                 
607                 //Insert the account selection view at the top of our view
608                 [[shelfView contentView] addSubview:view_accountSelection];
609                 [view_accountSelection setChat:chat];
611                 [[NSNotificationCenter defaultCenter] addObserver:self
612                                                                                                  selector:@selector(accountSelectionViewFrameDidChange:)
613                                                                                                          name:AIViewFrameDidChangeNotification
614                                                                                                    object:view_accountSelection];
615                 
616                 [self updateFramesForAccountSelectionView];
617                         
618                 //Redisplay everything
619                 [[shelfView contentView] setNeedsDisplay:YES];
620         }
624  * @brief Hide the account selection view
625  */
626 - (void)_destroyAccountSelectionView
628         if (view_accountSelection) {
629                 //Remove the observer
630                 [[NSNotificationCenter defaultCenter] removeObserver:self
631                                                                                                                 name:AIViewFrameDidChangeNotification
632                                                                                                           object:view_accountSelection];
634                 //Remove the account selection view from our window, clean it up
635                 [view_accountSelection removeFromSuperview];
636                 [view_accountSelection release]; view_accountSelection = nil;
638                 //Redisplay everything
639                 [self updateFramesForAccountSelectionView];
640         }
644  * @brief Position the account selection view, if it is present, and the messages/text entry splitview appropriately
645  */
646 - (void)updateFramesForAccountSelectionView
648         int             contentsHeight = [[shelfView contentView] frame].size.height;
649         int     accountSelectionHeight = (view_accountSelection ? [view_accountSelection frame].size.height : 0);
650         int             intersectionPoint = ([[shelfView contentView] isFlipped] ? accountSelectionHeight : (contentsHeight - accountSelectionHeight));
652         if (view_accountSelection) {
653                 [view_accountSelection setFrameOrigin:NSMakePoint(NSMinX([view_accountSelection frame]), intersectionPoint)];
654                 [view_accountSelection setNeedsDisplay:YES];
655         }
657         [splitView_textEntryHorizontal setFrameSize:NSMakeSize(NSWidth([splitView_textEntryHorizontal frame]), intersectionPoint)];
658         [splitView_textEntryHorizontal setNeedsDisplay:YES];
659 }       
662 //Text Entry -----------------------------------------------------------------------------------------------------------
663 #pragma mark Text Entry
665  * @brief Preferences changed, update sending keys
666  */
667 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object
668                                         preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
670         [textView_outgoing setSendOnReturn:[[prefDict objectForKey:SEND_ON_RETURN] boolValue]];
671         [textView_outgoing setSendOnEnter:[[prefDict objectForKey:SEND_ON_ENTER] boolValue]];
675  * @brief Configure the text entry view
676  */
677 - (void)_configureTextEntryView
678 {       
679         //Configure the text entry view
680     [textView_outgoing setTarget:self action:@selector(sendMessage:)];
682         //This is necessary for tab completion.
683         [textView_outgoing setDelegate:self];
684     
685         [textView_outgoing setTextContainerInset:NSMakeSize(0,2)];
686     if ([textView_outgoing respondsToSelector:@selector(setUsesFindPanel:)]) {
687                 [textView_outgoing setUsesFindPanel:YES];
688     }
689         [textView_outgoing setClearOnEscape:YES];
690         [textView_outgoing setTypingAttributes:[[adium contentController] defaultFormattingAttributes]];
691         
692         //User's choice of mininum height for their text entry view
693         entryMinHeight = [[[adium preferenceController] preferenceForKey:KEY_ENTRY_TEXTVIEW_MIN_HEIGHT
694                                                                                                                            group:PREF_GROUP_DUAL_WINDOW_INTERFACE] intValue];
695         if (entryMinHeight <= 0) entryMinHeight = [self _textEntryViewProperHeightIgnoringUserMininum:YES];
697         //Associate the view with our message view so it knows which view to scroll in response to page up/down
698         //and other special key-presses.
699         [textView_outgoing setAssociatedView:[messageDisplayController messageScrollView]];
700         
701         //Associate the text entry view with our chat and inform Adium that it exists.
702         //This is necessary for text entry filters to work correctly.
703         [textView_outgoing setChat:chat];
704         
705     //Observe text entry view size changes so we can dynamically resize as the user enters text
706     [[NSNotificationCenter defaultCenter] addObserver:self
707                                                                                          selector:@selector(outgoingTextViewDesiredSizeDidChange:)
708                                                                                                  name:AIViewDesiredSizeDidChangeNotification 
709                                                                                            object:textView_outgoing];
711         [self _updateTextEntryViewHeight];
715  * @brief Sets our text entry view as the first responder
716  */
717 - (void)makeTextEntryViewFirstResponder
719     [[textView_outgoing window] makeFirstResponder:textView_outgoing];
723  * @brief Clear the message entry text view
724  */
725 - (void)clearTextEntryView
727         NSWritingDirection      writingDirection;
729         writingDirection = [textView_outgoing baseWritingDirection];
730         
731         [textView_outgoing setString:@""];
732         [textView_outgoing setTypingAttributes:[[adium contentController] defaultFormattingAttributes]];
733         
734         [textView_outgoing setBaseWritingDirection:writingDirection];   //Preserve the writing diraction
736     [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification
737                                                                                                                 object:textView_outgoing];
741  * @brief Add text to the message entry text view 
743  * Adds the passed string to the entry text view at the insertion point.  If there is selected text in the view, it
744  * will be replaced.
745  */
746 - (void)addToTextEntryView:(NSAttributedString *)inString
748     [textView_outgoing insertText:inString];
749     [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:textView_outgoing];
753  * @brief Add data to the message entry text view 
755  * Adds the passed pasteboard data to the entry text view at the insertion point.  If there is selected text in the
756  * view, it will be replaced.
757  */
758 - (void)addDraggedDataToTextEntryView:(id <NSDraggingInfo>)draggingInfo
760     [textView_outgoing performDragOperation:draggingInfo];
761     [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:textView_outgoing];
765  * @brief Update the text entry view's height when its desired size changes
766  */
767 - (void)outgoingTextViewDesiredSizeDidChange:(NSNotification *)notification
769         [self _updateTextEntryViewHeight];
772 - (void)tabViewDidChangeVisibility
774         [self _updateTextEntryViewHeight];
777 /* 
778  * @brief Update the height of our text entry view
780  * This method sets the height of the text entry view to the most ideal value, and adjusts the other views in our
781  * window to fill the remaining space.
782  */
783 - (void)_updateTextEntryViewHeight
785         int             height = [self _textEntryViewProperHeightIgnoringUserMininum:NO];
786         
787         //Display the vertical scroller if our view is not tall enough to display all the entered text
788         [scrollView_outgoing setHasVerticalScroller:(height < [textView_outgoing desiredSize].height)];
790         if ([NSApp isOnLeopardOrBetter]) {
791                 //Attempt to maximize the message view's size.  We'll automatically restrict it to the correct minimum via the NSSplitView's delegate methods.
792                 [splitView_textEntryHorizontal adjustSubviews];
793                 [splitView_textEntryHorizontal setPosition:(NSHeight([splitView_textEntryHorizontal frame]) - height)
794                                                                   ofDividerAtIndex:0];
795                 
796         } else {
797                 NSRect  tempFrame, newFrame;
798                 BOOL    changed = NO;
800                 //Size the outgoing text view to the desired height
801                 tempFrame = [scrollView_outgoing frame];
802                 newFrame = NSMakeRect(tempFrame.origin.x,
803                                                           [splitView_textEntryHorizontal frame].size.height - height,
804                                                           tempFrame.size.width,
805                                                           height);
806                 if (!NSEqualRects(tempFrame, newFrame)) {
807                         [scrollView_outgoing setFrame:newFrame];
808                         [scrollView_outgoing setNeedsDisplay:YES];
809                         changed = YES;
810                 }
812                 if (changed) {
813                         [splitView_textEntryHorizontal adjustSubviews];
814                 }
815         }
819  * @brief Returns the height our text entry view should be
821  * This method takes into account user preference, the amount of entered text, and the current window size to return
822  * a height which is most ideal for the text entry view.
824  * @param ignoreUserMininum If YES, the user's preference for mininum height will be ignored
825  */
826 - (int)_textEntryViewProperHeightIgnoringUserMininum:(BOOL)ignoreUserMininum
828         int dividerThickness = [splitView_textEntryHorizontal dividerThickness];
829         int allowedHeight = ([splitView_textEntryHorizontal frame].size.height / 2.0) - dividerThickness;
830         int     height;
831         
832         //Our primary goal is to display all the entered text
833         height = [textView_outgoing desiredSize].height;
835         //But we must never fall below the user's prefered mininum or above the allowed height
836         if (!ignoreUserMininum && height < entryMinHeight) {
837                 height = entryMinHeight;
838         }
839         if (height > allowedHeight) height = allowedHeight;
840         
841         return height;
844 #pragma mark Autocompletion
846  * @brief Should the tab key cause an autocompletion if possible?
848  * We only tab to autocomplete for a group chat
849  */
850 - (BOOL)textViewShouldTabComplete:(NSTextView *)inTextView
852         return [[self chat] isGroupChat];
855 - (NSArray *)textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index
857         NSMutableArray  *completions;
858         
859         if ([[self chat] isGroupChat]) {
860                 NSString                *partialWord = [[[textView textStorage] attributedSubstringFromRange:charRange] string];
861                 NSEnumerator    *enumerator;
862                 AIListContact   *listContact;
863                 
864                 NSString                *suffix;
865                 if (charRange.location == 0) {
866                         //At the start of a line, append ": "
867                         suffix = @": ";
868                 } else {
869                         suffix = nil;
870                 }
871                 
872                 completions = [NSMutableArray array];
873                 enumerator = [[[self chat] containedObjects] objectEnumerator];
874                 while ((listContact = [enumerator nextObject])) {
875                         if ([[listContact displayName] rangeOfString:partialWord
876                                                                                                  options:(NSLiteralSearch | NSAnchoredSearch)].location != NSNotFound) {
877                                 
878                                 [completions addObject:(suffix ? [[listContact displayName] stringByAppendingString:suffix] : [listContact displayName])];
879                                 
880                         } else if ([[listContact formattedUID] rangeOfString:partialWord
881                                                                                                                  options:(NSLiteralSearch | NSAnchoredSearch)].location != NSNotFound) {
882                                 [completions addObject:(suffix ? [[listContact formattedUID] stringByAppendingString:suffix] : [listContact formattedUID])];
883                                 
884                         } else if ([[listContact UID] rangeOfString:partialWord
885                                                                                                 options:(NSLiteralSearch | NSAnchoredSearch)].location != NSNotFound) {
886                                 [completions addObject:(suffix ? [[listContact UID] stringByAppendingString:suffix] : [listContact UID])];
887                         }
888                 }
890                 if ([completions count]) {                      
891                         *index = 0;
892                 }
894         } else {
895                 completions = nil;
896         }
898         return ([completions count] ? completions : words);
901 //User List ------------------------------------------------------------------------------------------------------------
902 #pragma mark User List
904  * @brief Set visibility of the user list
905  */
906 - (void)setUserListVisible:(BOOL)inVisible
908         if (inVisible) {
909                 [self _showUserListView];
910         } else {
911                 [self _hideUserListView];
912         }
916  * @brief Returns YES if the user list is currently visible
917  */
918 - (BOOL)userListVisible
920         return [shelfView isShelfVisible];
924  * @brief Show the user list
925  */
926 - (void)_showUserListView
927 {       
928         [self setupShelfView];
930         //Configure the user list
931         [self _configureUserList];
933         //Add the user list back to our window if it's missing
934         if (![self userListVisible]) {
935                 [self _updateUserListViewWidth];
936                 
937                 if (retainingScrollViewUserList) {
938                         [scrollView_userList release];
939                         retainingScrollViewUserList = NO;
940                 }
941         }
945  * @brief Hide the user list.
947  * We gain responsibility for releasing scrollView_userList after we hide it
948  */
949 - (void)_hideUserListView
951         if ([self userListVisible]) {
952                 [scrollView_userList retain];
953                 [scrollView_userList removeFromSuperview];
954                 retainingScrollViewUserList = YES;
955                 
956                 [self saveUserListMinimumSize];
957                 [userListController release];
958                 userListController = nil;
959         
960                 //need to collapse the splitview
961                 [shelfView setShelfIsVisible:NO];
962         }
966  * @brief Configure the user list
968  * Configures the user list view and prepares it for display.  If the user list is not being shown, this configuration
969  * should be avoided for performance.
970  */
971 - (void)_configureUserList
973         if (!userListController) {
974                 NSDictionary    *themeDict = [NSDictionary dictionaryNamed:USERLIST_THEME forClass:[self class]];
975                 NSDictionary    *layoutDict = [NSDictionary dictionaryNamed:USERLIST_LAYOUT forClass:[self class]];
976                 
977                 //Create and configure a controller to manage the user list
978                 userListController = [[ESChatUserListController alloc] initWithContactListView:userListView
979                                                                                                                                                   inScrollView:scrollView_userList 
980                                                                                                                                                           delegate:self];
981                 [userListController updateLayoutFromPrefDict:layoutDict andThemeFromPrefDict:themeDict];
982                 [userListController setContactListRoot:chat];
983                 [userListController setHideRoot:YES];
985                 //User's choice of mininum width for their user list view
986                 userListMinWidth = [[[adium preferenceController] preferenceForKey:KEY_ENTRY_USER_LIST_MIN_WIDTH
987                                                                                                                                          group:PREF_GROUP_DUAL_WINDOW_INTERFACE] intValue];
988                 if (userListMinWidth < USER_LIST_MIN_WIDTH) userListMinWidth = USER_LIST_DEFAULT_WIDTH;
989                 [shelfView setShelfWidth:[userListView bounds].size.width];
990         }
994  * @brief Update the user list in response to changes
996  * This method is invoked when the chat's participating contacts change.  In resopnse, it sets correct visibility of
997  * the user list, and updates the displayed users.
998  */
999 - (void)chatParticipatingListObjectsChanged:(NSNotification *)notification
1001     //Update the user list
1002         AILogWithSignature(@"%i, so %@ %@",[self userListVisible], ([self userListVisible] ? @"reloading" : @"not reloading"),
1003                                            userListController);
1004     if ([self userListVisible]) {
1005         [userListController reloadData];
1006     }
1010  * @brief The selection in the user list changed
1012  * When the user list selection changes, we update the chat's "preferred list object", which is used
1013  * elsewhere to identify the currently 'selected' contact for Get Info, Messaging, etc.
1014  */
1015 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
1017         if ([notification object] == userListView) {
1018                 int selectedIndex = [userListView selectedRow];
1019                 [chat setPreferredListObject:((selectedIndex != -1) ? 
1020                                                                           [[chat containedObjects] objectAtIndex:selectedIndex] :
1021                                                                           nil)];
1022         }
1026  * @brief Perform default action on the selected user list object
1028  * Here we could open a private message or display info for the user, however we perform no action
1029  * at the moment.
1030  */
1031 - (void)performDefaultActionOnSelectedObject:(AIListObject *)listObject sender:(NSOutlineView *)sender
1033         //Empty
1036 /* 
1037  * @brief Update the width of our user list view
1039  * This method sets the width of the user list view to the most ideal value, and adjusts the other views in our
1040  * window to fill the remaining space.
1041  */
1042 - (void)_updateUserListViewWidth
1044         int             width = [self _userListViewProperWidthIgnoringUserMininum:NO];
1045         int             widthWithDivider = 1 + width;   //resize bar effective width  
1046         NSRect  tempFrame;
1048         //Size the user list view to the desired width
1049         tempFrame = [scrollView_userList frame];
1050         [scrollView_userList setFrame:NSMakeRect([shelfView frame].size.width - width,
1051                                                                                          tempFrame.origin.y,
1052                                                                                          width,
1053                                                                                          tempFrame.size.height)];
1054         
1055         //Size the message view to fill the remaining space
1056         tempFrame = [scrollView_messages frame];
1057         [scrollView_messages setFrame:NSMakeRect(tempFrame.origin.x,
1058                                                                                          tempFrame.origin.y,
1059                                                                                          [shelfView frame].size.width - widthWithDivider,
1060                                                                                          tempFrame.size.height)];
1062         //Redisplay both views and the divider
1063         [shelfView setNeedsDisplay:YES];
1067  * @brief Returns the width our user list view should be
1069  * This method takes into account user preference and the current window size to return a width which is most
1070  * ideal for the user list view.
1072  * @param ignoreUserMininum If YES, the user's preference for mininum width will be ignored
1073  */
1074 - (int)_userListViewProperWidthIgnoringUserMininum:(BOOL)ignoreUserMininum
1076         int dividerThickness = 1; //[shelfView dividerThickness];
1077         int allowedWidth = ([shelfView frame].size.width / 2.0) - dividerThickness;
1078         int     width = USER_LIST_MIN_WIDTH;
1079         
1080         //We must never fall below the user's prefered mininum or above the allowed width
1081         if (!ignoreUserMininum && width < userListMinWidth) width = userListMinWidth;
1082         if (width > allowedWidth) width = allowedWidth;
1084         return width;
1088 //Split Views --------------------------------------------------------------------------------------------------
1089 #pragma mark Split Views
1090 /* 
1091  * @brief Returns the maximum constraint of the split pane
1093  * For the horizontal split, we prevent the message view from growing so large that the text entry view
1094  * is forced below its desired height.
1095  */
1096 - (float)splitView:(NSSplitView *)sender constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)offset
1098         if (sender == splitView_textEntryHorizontal) {          
1099                 return ([sender frame].size.height - ([self _textEntryViewProperHeightIgnoringUserMininum:YES] +
1100                                                                                          [sender dividerThickness]));
1102         } else {
1103                 NSLog(@"Unknown split view %@",sender);
1104                 return 0;
1105         }
1108 /* 
1109  * @brief Returns the mininum constraint of the split pane
1111  * For both splitpanes, we prevent the message view from dropping below 50% of the window's width and height
1112  */
1113 - (float)splitView:(NSSplitView *)sender constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)offset
1115         if (sender == splitView_textEntryHorizontal) {
1116                 return (int)([sender frame].size.height * MESSAGE_VIEW_MIN_HEIGHT_RATIO);
1117                 
1118         } else {
1119                 NSLog(@"Unknown split view %@",sender);
1120                 return 0;
1121         }
1125  * @brief A split view had its divider position changed
1127  * Remember the user's choice of text entry view height.
1128  */
1129 - (float)splitView:(NSSplitView *)sender constrainSplitPosition:(float)proposedPosition ofSubviewAt:(int)index
1131         if (sender == splitView_textEntryHorizontal) {
1132                 entryMinHeight = (int)([sender frame].size.height - proposedPosition);
1133         } else {
1134                 NSLog(@"Unknown split view %@",sender);
1135                 return 0;
1136         }
1137         
1138         return proposedPosition;
1141 /* 
1142  * @brief Returns YES if the passed subview can be collapsed
1143  */
1144 - (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview
1146         if (sender == splitView_textEntryHorizontal) {
1147                 return NO;
1148                 
1149         } else {
1150                 NSLog(@"Unknown split view %@",sender);
1151                 return 0;
1152         }
1155 #pragma mark Shelfview
1156 /* @name        setupShelfView
1157  * @brief       sets up shelfsplitview containing userlist & contentviews
1158  */
1159  -(void)setupShelfView
1161         [shelfView setShelfWidth:200];
1163         AILogWithSignature(@"ShelfView %@ (content view is %@) --> superview %@, in window %@; frame %@; content view %@ shelf view %@ in window %@",
1164                                            shelfView, [shelfView contentView], [shelfView superview], [shelfView window], NSStringFromRect([[shelfView superview] frame]),
1165                                            splitView_textEntryHorizontal,
1166                                            scrollView_userList, [scrollView_userList window]);
1168         [shelfView bind:@"contextButtonMenu" toObject:[self chat] withKeyPath:@"actionMenu"
1169                         options:[NSDictionary dictionaryWithObjectsAndKeys:
1170                                          [NSNumber numberWithBool:YES], NSAllowsNullArgumentBindingOption,
1171                                          [NSNumber numberWithBool:YES], NSValidatesImmediatelyBindingOption,
1172                                          nil]];
1173         [shelfView setContextButtonImage:[NSImage imageNamed:@"sidebarActionWidget.png"]];
1175         [shelfView setShelfIsVisible:YES];
1178 /* @name        toggleUserlist
1179  * @brief       toggles the state of the userlist shelf
1180  */
1181 -(void)toggleUserlist:(id)sender
1182 {       
1183         [shelfView setShelfIsVisible:![shelfView isShelfVisible]];
1184 }       
1186 @end